import cv2 as cv
#构造边缘检测函数
'''
索贝尔算子(Sobel)主要用作边缘检测，在技术上，它是一离散性差分算子，用来运算图像亮度函数的灰度之近似值。在图像的任何一点使用此算子，
将会产生对应的灰度矢量或是其法矢量。
Sobel算子算法的优点是计算简单，速度快。但是由于只采用了2个方向的模板，只能检测水平和垂直方向的边缘，
因此这种算法对于纹理较为复杂的图像，其边缘检测效果就不是很理想。该算法认为：凡灰度新值大于或等于阈值的像素点时都是边缘点。
这种判断欠合理，会造成边缘点的误判，因为许多噪声点的灰度值也很大。
在Sobel函数的第二个参数这里使用了cv2.CV_16S。
因为OpenCV文档中对Sobel算子的介绍中有这么一句：
“in the case of 8-bit input images it will result in truncated derivatives”。
即Sobel函数求完导数后会有负值，还有会大于255的值。
而原图像是uint8，即8位无符号数，所以Sobel建立的图像位数不够，会有截断。
因此要使用16位有符号的数据类型，即cv2.CV_16S。
在经过处理后，别忘了用convertScaleAbs()函数将其转回原来的uint8形式。
否则将无法显示图像，而只是一副灰色的窗口。convertScaleAbs()的原型为：
dst = cv2.convertScaleAbs(src[, dst[, alpha[, beta]]])
其中可选参数alpha是伸缩系数，beta是加到结果上的一个值。结果返回uint8类型的图片。
由于Sobel算子是在两个方向计算的，最后还需要用cv2.addWeighted(...)函数将其组合起来。
其函数原型为：
dst = cv2.addWeighted(src1, alpha, src2, beta, gamma[, dst[, dtype]])
其中alpha是第一幅图片中元素的权重，beta是第二个的权重，gamma是加到最后结果上的一个值。

'''
def edge_sobel(image):
    x = cv.Sobel(image, cv.CV_16S, 1, 0)
    y = cv.Sobel(image, cv.CV_16S, 0, 1)
    absX = cv.convertScaleAbs(x)  # 转回uint8
    absY = cv.convertScaleAbs(y)
    dst = cv.addWeighted(absX, 0.5, absY, 0.5, 0)
    cv.imshow("sobel_edge", dst)  # 输出边缘检测图像
'''
Laplace算子 
图像中的边缘区域，像素值会发生“跳跃”，对这些像素求导，在其一阶导数在边缘位置为极值，这就是Sobel算子使用的原理——极值处就是边缘
如果对像素值求二阶导数，会发现边缘处的导数值为0。
Laplace函数实现的方法是先用Sobel 算子计算二阶x和y导数，再求和：
'''
def edge_laplacian(image):
    # 使用高斯滤波消除噪声
    blurred = cv.GaussianBlur(image, (3, 3), 0, 0)
    cv.imshow("laplacian_edge", blurred)
    cv.imshow("laplacian_edge", image)
    #使用Laplace函数
    gray_lap = cv.Laplacian(blurred, cv.CV_16S, ksize=3)

    dst = cv.convertScaleAbs(gray_lap)  # 转回uint8

    #edge_output = cv.Laplacian(gray, cv.CV_16S, 3, 1, 0)
    #edge_output = cv.Laplacian(gray, cv.CV_16S, ksize=3)  # Laplacian算子边缘检测
    #cv.namedWindow('laplacian_edge', cv.WINDOW_NORMAL)
    cv.imshow("laplacian_edge", dst)  # 输出边缘检测图像
'''
canny算法必要参数：
第一个参数是需要处理的原图像，该图像必须为单通道的灰度图；
第二个参数是阈值1；
第三个参数是阈值2。
其中较大的阈值2用于检测图像中明显的边缘，但一般情况下检测的效果不会那么完美，边缘检测出来是断断续续的。
所以这时候用较小的第一个阈值用于将这些间断的边缘连接起来。
可选参数中apertureSize就是Sobel算子的大小。而L2gradient参数是一个布尔值，如果为真，则使用更精确的L2范数进行计算
（即两个方向的倒数的平方和再开放），否则使用L1范数（直接将两个方向导数的绝对值相加）。
函数返回一副二值图，其中包含检测出的边缘。

canny算法步骤：
　　①高斯模糊 - GaussianBlur
　　②灰度转换 - cvtColor
　　③计算梯度 – Sobel/Scharr
　　④非最大信号抑制
　　⑤高低阈值输出二值图像
'''
def edge_canny(image):
    blurred = cv.GaussianBlur(image, (3, 3), 0)  # 经过高斯滤波
    # xgrad = cv.Sobel(gray, cv.CV_16SC1, 1, 0) #x方向梯度
    # ygrad = cv.Sobel(gray, cv.CV_16SC1, 0, 1) #y方向梯度
    # edge_output = cv.Canny(xgrad, ygrad, 50, 150)
    edge_output = cv.Canny(image, 50, 150)  # Canny算子边缘检测
    #该函数对两个图像的对应元素值逐一进行“与”操作，由于蒙板的存在，该函数将不会对蒙板覆盖的元素进行计算
   # dst = cv.bitwise_and(image, image, mask=edge_output)
    #cv.namedWindow('canny_edge', cv.WINDOW_NORMAL)
    cv.imshow("canny_edge", edge_output)  # 输出边缘检测图像
    #cv.imshow("canny_edge", dst)

#在运行时调整canny算法阈值大小观测图像变化
def CannyThreshold(lowThreshold):
    detected_edges = cv.GaussianBlur(gray_img, (3, 3), 0)
    detected_edges = cv.Canny(detected_edges, lowThreshold, lowThreshold * ratio, apertureSize=kernel_size)
    #dst = cv.bitwise_and(src, src, mask=detected_edges)  # just add some colours to edges from original image.
    cv.imshow('canny demo', detected_edges)

src = cv.imread('./lena.jpg')
cv.namedWindow('src_image', cv.WINDOW_NORMAL)  # 设置为WINDOW_NORMAL可以任意缩放
cv.imshow('src_image', src)
gray_img = cv.cvtColor(src, cv.COLOR_RGB2GRAY)  # 经过RGB灰度转换
edge_sobel(gray_img)
edge_laplacian(gray_img)
edge_canny(gray_img)
cv.waitKey(0)
cv.destroyAllWindows()

#调用变化canny参数函数
lowThreshold = 0
max_lowThreshold = 100
ratio = 3
kernel_size = 3
cv.namedWindow('canny demo')
cv.createTrackbar('Min threshold', 'canny demo', lowThreshold, max_lowThreshold, CannyThreshold)
CannyThreshold(0)  # initialization
if cv.waitKey(0) == 27:
    cv.destroyAllWindows()